xun su
11/13/2023, 10:52 PM// Base class for documents
open class Document {
open fun printContent() = "Printing generic document content"
}
// PDF document subclass
class PdfDocument : Document() {
override fun printContent() = "Printing PDF content"
}
// Word document subclass
class WordDocument : Document() {
override fun printContent() = "Printing Word content"
}
// Printer interface, contravariant in T
interface Printer<in T> {
fun print(document: T)
}
// Implementation of a printer that can print any type of document
class GeneralPrinter : Printer<Document> {
override fun print(document: Document) {
println(document.printContent())
}
}
// Implementation of a printer that is specialized for PDF documents
class PdfPrinter : Printer<PdfDocument> {
override fun print(document: PdfDocument) {
println("PDF Printer: ${document.printContent()}")
}
}
fun main() {
val pdfDocument = PdfDocument()
val wordDocument = WordDocument()
val generalPrinter: Printer<Document> = GeneralPrinter()
val pdfPrinter: Printer<PdfDocument> = PdfPrinter()
// PdfPrinter can print PdfDocument
pdfPrinter.print(pdfDocument)
// GeneralPrinter can print any document
generalPrinter.print(pdfDocument)
generalPrinter.print(wordDocument)
// Thanks to contravariance, we can assign a PdfPrinter to a Printer<Document>
val printer: Printer<Document> = PdfPrinter() // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! error here in kotlin playground
printer.print(pdfDocument) // Valid, PdfDocument is a Document
// This line would result in a compile-time error if uncommented
// printer.print(wordDocument) // Invalid, WordDocument is not a PdfDocument
}
the error is "Type mismatch: inferred type is PdfPrinter but Printer<Document> was expected" , even if the code works I still don't know why not define the interface like this:
// without using the keyword in
interface Printer<T> {
fun print(document: T)
}
so, could anyone tell me what is the best practice to use the keyword in
which means Contravariance ( please compare with not using in
might have what kind of risks )Stephan Schröder
11/13/2023, 11:29 PMCasey Brooks
11/13/2023, 11:32 PMPrinter
class uses in
to declare that it can take in a value of any kind of Document
, but when you extend it with PdfPrinter
, that no longer holds true. The PdfPrinter
subclass now restricts the types that it is able to take in to only PdfDocument
. You cannot assign PdfPrinter
to Printer<Document>
because you cannot pass any kind of document to it. You can only pass in a PdfDocument
. So it would not make sense to be able to assign PdfPrinter
to Printer<Document>
, because that would allow you to try and pass a WordDocument
in, for example, which would then throw a ClassCastException
because the WordDocument
is not a PdfDocument
.
To extend the example in a way that might help you understand, let’s look at an example using out
variance.
interface Loader<out T: Document> {
fun load(fileName: String): T
}
class PdfLoader : Loader<PdfDocument> {
override fun load(fileName: String): PdfDocument = TODO()
}
class WordDocumentLoader : Loader<WordDocument> {
override fun load(fileName: String): WordDocument = TODO()
}
Here, you are able to assign PdfLoader
to Loader<Document>
, since load()
returns an instance of a PdfDocument
. You can safely assign PdfDocument
to a variable of type Document
, and therefore there’s nothing unsafe with treating a Loader<PdfDocument>
as just Loader<Document>
. It’s not that the Loader suddenly gains the ability to load any type of document (it still only returns instances of PdfDocument
), but you are declaring your intent to be that you do not care about the subtype, and are only concerned with the base Document
.
val loader: Loader<Document> = PdfLoader()
val pdfDocument: Document = loader.load("document1.pdf")
val generalDocument: PdfDocument = loader.load("document1.pdf") // error, loader is only aware that it returns a `Document`
And to bring the two together, removing the variance bounds of the generic type parameter basically just treats it as being both in
and out
simultaneously. For example, you can create an interface that extends both Printer
and Loader
with the same type variable as long as it doesn’t have variance
// does not compile is T is `in` or `out`
interface DocumentContext<T: Document> : Loader<T>, Printer<T>
public class PdfHelper : DocumentHelper<PdfDocument> {
override fun print(document: PdfDocument) { TODO() }
override fun load(fileName: String): PdfDocument = TODO()
}
val context: DocumentHelper<PdfDocument> = PdfHelper()
val loader: Loader<Document> = context
val pdfLoader: Loader<PdfDocument> = context
val printer: Printer<Document> = context // error, cannot pass any generic Document. Limited to PdfDocumentes
val pdfPrinter: Printer<PdfDocument> = context
Jacob
11/14/2023, 12:26 AMxun su
11/15/2023, 10:49 AMinterface Sender<in T : Message> {
fun send(message: T)
}
class GeneralSender(serviceUrl: String) : Sender<Message> {
private val connection = Connection("")
override fun send(message: Message) {
connection.send(message)
}
}
fun main() {
val orderManagerSender: Sender<OrderManagerMessage> = GeneralSender("orderManagerURL")
orderManagerSender.send(OrderManagerMessageImpl())
}
to this
interface Sender<T : Message> {
fun send(message: T)
}
class GeneralSender<T : Message>(serviceUrl: String) : Sender<T> {
private val connection = Connection("")
override fun send(message: T) {
connection.send(message)
}
}
fun main() {
val orderManagerSender: Sender<OrderManagerMessage> = GeneralSender("orderManagerURL")
orderManagerSender.send(OrderManagerMessageImpl())
val invoiceManagerSender: Sender<InvoiceManagerMessage> = GeneralSender("invoiceManagerURL")
invoiceManagerSender.send(InvoiceManagerMessageImpl())
}
it seems we can do the same thing without using in
@Stephan Schröder
Stephan Schröder
11/17/2023, 12:54 PMinterface Source<out T>
and
interface Sink<in T>
You might want to reread the documentation about generics 😅